Qt5.2开发文档翻译:状态机框架,The State Machine Framework
状态 机框架,提供了一些类,用来创建及执行状态图。相关 的概念及术语,是基于Harel 的论文 状态 图: 以可视的方式将复杂系统形式化 的, 该论文也是UML 状态图的基础。状态 机执行的语言,是基于 状态 图 XML (SCXML) 的。
状态图,提供了一种图形化的方式,用来表达,系统如何对刺激进行响应。具体做法就是,定义系统可能存在的 状态 ,以及,系统如何从一个状态移动到另一个状态(状态之间的 迁移 )。对于事件驱动的系统(例如Qt应用程序),有一个关键特性就是,它的行为,通常并不仅仅取决于上一个或当前事件,还取决于之前的那些事件。利用状态图,狠容易表达这种信息。
状态机框架提供了一个应用编程接口和执行模型,可以用来高效地将状态图的元素和语义嵌入到Qt 程序中去。这个框架与Qt 的元对象系统紧密结合;例如,状态之间的迁移是可以通过信号来触发的,并且,各个状态可配置为设置QObject的属性和调用QObject的方法。Qt的事件系统被用来驱动状态机。
状态机框架中的状态图是层次型的。状态可被嵌套到其它状态中,同时,状态机的当前配置,是由当前活跃的状态集合组成的。在一个有效配置的状态机中,所有的状态都有一个共有的祖先。
qt 提供了以下类,以用来创建事件驱动的状态机。
QKeyEventTransition类,提供了一个针对按键事件的迁移动作。 |
|
QMouseEventTransition类,提供了一个针对鼠标事件的迁移动作。 |
|
QStateMachine中的状态的基类。 |
|
不同QAbstractState 对象之间的迁移动作的基类。 |
|
与QObject相关的由Qt 事件触发的迁移。 |
|
终止状态。 |
|
用来返回之前的某个活跃子状态的手段。 |
|
基于Qt 信号的迁移。 |
|
QStateMachine的通用目的状态。 |
|
多层次的有限状态机。 |
|
代表一个Qt信号事件。 |
|
继承自QEvent,容纳着一个与某个QObject相关联的事件的副本。 |
让我们来看一个简单的示例,以展示状态机应用编程接口的核心功能: 这个状态机拥有3个状态, s1 、 s2 和 s3 。 这个状态机是由一个 QPushButton 控制的; 当该按钮被按时,状态机就会迁移到另一个状态。最初 ,状态机的状态为 s1 。该状态机的状态图如下所示:
以下就是创建这么一个状态机所需的代码。首先,我们创建该状态机及它的状态:
QStateMachine machine;
然后 ,我们使用 QState::addTransition ()函数来创建迁移动作:
s1->addTransition(button, SIGNAL(clicked()), s2);
s2->addTransition(button, SIGNAL(clicked()), s3);
s3->addTransition(button, SIGNAL(clicked()), s1);
接下来,我们将各个状态添加到状态机中,并且设置状态机的初始状态:
machine.addState(s1);
machine.addState(s2);
machine.addState(s3);
machine.setInitialState(s1);
最后,启动这个状态机:
machine.start();
状态机会异步地执行,也就是说,它会融入妳的程序的事件循环中。
前面所说的那个状态机,只是在不同状态之间迁移, 它不会进行任何 操 作。 可使用 QState::assignProperty ()函数 来让某个状态对象 在进入 该 状态时设置某个 QObject 的一个属性。 在以下代码片断中,每个状态对象 ,在进入该状态时,都会 给某个 QLabel 的文本内容(text)属性设置一个值:
s1->assignProperty(label, "text", "In state s1");
s2->assignProperty(label, "text", "In state s2");
s3->assignProperty(label, "text", "In state s3");
在进入这些状态时,该文本标签(label)的文本内容会对应地发生改变。
当状态被进入时,会发射 QState::entered ()信号,当状态被离开时,会发射 QState::exited ()信号。 在以下代码片断中, 当进入状态 s3 时,按钮(button)的showMaximized()信号槽会被调用, 当离开状态 s3 时,按钮(button)的showMinimized()信号槽会被调用。
QObject ::connect(s3, SIGNAL(entered()), button, SLOT(showMaximized()));
QObject ::connect(s3, SIGNAL(exited()), button, SLOT(showMinimized()));
自定义的状态,可覆盖 QAbstractState::onEntry ()和 QAbstractState::onExit ()方法。
上一小节中定义的状态机永远不会终止。 要想让一个状态机成为可终止的,则,需要 给它添加一个顶级 的 终止 状态( QFinalState 对象)。 当状态机进入某个顶级终止状态时,会发射 QStateMachine::finished ()信号,并且停机。
要想在图中引入一个终止状态,只需要创建 一个 QFinalState 对象,并且将它设置为一个或多个迁移动作的目标。
假设 ,我们希望,任何时候当用户点击一个退出(Quit)按钮时,程序 都退出。 要想实现这一点, 我们需要创建一个终止状态,并且 让它成为 与 退出 (Quit) 按钮 的clicked()信号相关联的迁移动作的目标。 我们是可以针对 s1 、 s2 和 s3 分别添加一个迁移动作;但是, 这似乎有点多余,并且 ,日后再加入 新的状态的话, 还必须记得要添加对应 的迁移动作。
我们可以将 s1 、 s2 和 s3 这 3个状态归入一分组,就能够实现同样的效果(点击退出 ( Quit )按钮就会退出整个状态机,而不用管它当时处于什么状态 )。具体做法 就是,创建一个新的顶级状态 , 并将原来那3个状态设置为新状态的子状态。 以下图片表示了新的状态机。
原来 的3个状态被改名为 s11 、 s12 和 s13 ,以表示,它们现在是 新的顶级状态 s1 的子状态。 子状态会隐式地继承它们 的亲代状态的迁移动作。 这就意味着,现在,只需要加入单个 从 s1 指向终止状态 s2 的迁移动作就可以了。 向 s1 中添加的新状态也会自动继承这个迁移动作。
对状态进行分组狠简单,只需要在该状态被创建时,指定一个适当的亲代状态就可以了。另外,还需要指定,哪个子状态作为初始状态(也就是说,当亲代状态成为某个迁移动作的目标时,状态机应当进入哪个子状态)。
QState *s11 = new QState (s1);
QState *s12 = new QState (s1);
QState *s13 = new QState (s1);
s1->setInitialState(s11);
machine.addState(s1);
QFinalState *s2 = new QFinalState ();
s1->addTransition(quitButton, SIGNAL(clicked()), s2);
machine.addState(s2);
machine.setInitialState(s1);
QObject ::connect(&machine, SIGNAL(finished()), QApplication ::instance(), SLOT(quit()));
在这个示例中,我们希望,当状态机结束时,退出程序,因此,我们将状态机的finished()信号连接到程序的quit()信号槽。
子状态可以覆盖掉它继承来的迁移动作。例如, 以下代码中, 会添加一个新的迁移动作, 它将导致,当状态机处于 s12 状态时,退出(Quit)按钮会被忽略。
s12->addTransition(quitButton, SIGNAL(clicked()), s12);
迁移动作可将任何状态设置为它的目标,也就是说,目标状态不需要与源状态处于同一个层级。
假设 ,我们需要向 前一小节中说明的示例中加入一个“中断”机制;用户 可以点击某个按钮,以 让状态机做点 不相关的工作,之后,状态 机应当继续进行它之前的事务( 也就是说,返回到旧的状态, 在这个示例中,就是 s11 、 s12 和 s13 中的某个 )。
这种行为,可利用 历史状态 来轻易实现。历史状态 ( QHistoryState 对象),是一个伪状态,代表 着 ,某个亲代状态上次退出之前 所处于其中的子状态。
历史状态,在创建时,应当指定为我们希望记录其当前子状态的那个状态的子状态;当状态机在运行时检测到这样一个状态的存在时,会在亲代状态退出时自动记录当前(真正的)子状态。指向该历史状态的迁移动作,实际上会指向该状态机之前保存的那个子状态;状态机会自动将迁移动作“转发”给真正的子状态。
以下图片展示了加入中断机制之后的状态机。
以下代码展示了如何实现这种行为; 在这个示例中,我们只是在进入 s3 状态时显示一个消息框,然后立即通过历史状态返回 到 s1 之前的子状态。
QHistoryState *s1h = new QHistoryState (s1);
s3->assignProperty(label, "text", "In s3");
QMessageBox *mbox = new QMessageBox (mainWindow);
mbox->addButton( QMessageBox ::Ok);
mbox->setText("Interrupted!");
mbox->setIcon( QMessageBox ::Information);
QObject ::connect(s3, SIGNAL(entered()), mbox, SLOT(exec()));
s3->addTransition(s1h);
machine.addState(s3);
s1->addTransition(interruptButton, SIGNAL(clicked()), s3);
假设,妳想在单个状态机中管理一辆汽车的多个互斥属性。比如说,我们关心两组属性:干净(Clean)和脏脏(Dirty);以及正在移动(Moving)和未移动(Not moving)。将需要4个互斥的状态,和8个迁移动作,才能表示出来,并且自由地在所有可能的组合之间迁移。
如果我们再添加第3个属性(例如,红色(Red)和蓝色(Blue)),那么,总的状态数就会翻倍,变成8;好了,如果我们再添加第4个属性(例如,闭篷的(Enclosed)和敞篷的(Convertible)),那么,总的状态数又将翻倍,变成16。
使用并行状态的话,就可以使得,当我们添加更多属性时,总的状态数线性增长,而不是指数增长。并且,在向并行状态中添加或删除状态时,不会影响到相邻的状态。
要创建一个并行状态 分 组,则,在 QState 构造函数中传入 QState::ParallelStates 参数。
QState *s1 = new QState ( QState ::ParallelStates);
// s11 和 s12 会被并行地进入
QState *s11 = new QState (s1);
QState *s12 = new QState (s1);
当状态机进入一个并行状态分组时,所有的子状态都会被并行地进入。在单个子状态之间的迁移,还是照常进行。然而,任何一个子状态都可能因为一个迁移动作而离开亲代状态。当发生这种事的时候,亲代状态及它所有的子状态都会退出。
状态机框架的并行性是符合一种交错式语义的。所以的并行操作,都会在一个单个的、原子的事件处理步骤中执行,因此,不会有事件能够中断并行操作。但是,事件仍然是被串行处理的,因为状态机本身是单线程的。举个例子:假设,有两个迁移动作都会导致离开同一个并行状态分组,而它们的条件又同时被满足了。在这种情况下,两个事情中排在后面被处理的那个,不会起作用,因为,第一个事件已经引起状态机离开了并行状态。
子状态可以是终止状态( QFinalState 对象); 当某个终止子状态被进入时,亲代状态 会发射 QState::finished ()信号 。下图展示 的是一个复合状态 s1 ,它会在进入终止状态之前做一些处理:
当 s1 的终止状态被进入时, s1 会自动发射finished()信号。 我们使用一个信号迁移动作来 让这个事件触发状态变化动作:
s1->addTransition(s1, SIGNAL(finished()), s2);
当妳想要隐藏 一个复合状态内部的细节是,使用终止状态 是狠有用的; 也就是说,外界能够 做的事情就是,进入 该状态,然后 当该状态完成自己的工作的时候得到一个通知。 这是一种非常强大的抽象和封装机制, 可用于构建复杂(深层嵌套)的状态机。 (当然 ,在上面的示例中, 妳可以创建一个从 s1 的 done 状态直接发出的迁移动作, 而不是依赖于 s1 的finished()信号,但是,后果 就是, s1 的实现细节被暴露出来了,并且外界会依赖该细节 ) 。
对于并行状态分组, 当其中 所有的 子状态都进入终止状态时,会发射 QState::finished ()信号。
迁移动作并不一定要有一个目标状态。一个无目标的迁移动作,可像其它迁移动作一样被触发;区别就是,当一个无目标的迁移动作被触发时,不会引起状态变更。这样,当妳的状态机处于特定状态时,可以对信号或事件进行响应,而不离开该状态。示例:
QStateMachine machine;
QState *s1 = new QState (&machine);
QPushButton button;
QSignalTransition *trans = new QSignalTransition (&button, SIGNAL(clicked()));
s1->addTransition(trans);
QMessageBox msgBox;
msgBox.setText("The button was clicked; carry on.");
QObject ::connect(trans, SIGNAL(triggered()), &msgBox, SLOT(exec()));
machine.setInitialState(s1);
每当按钮 (button)被点击时,消息 框就会弹出,但是状态 机会保持在当前状态 (s1)。但是,如果显式 地将目标状态设置为 s1 的话,则,每次 , s1 都 会被离开然后重新进入 (例如, 会发射 QAbstractState::entered ()和 QAbstractState::exited ()信号 ) 。
QStateMachine 会运行着它自己的事件循环。对于信号迁移动作 ( QSignalTransition 对象) , QStateMachine 会在拦截到对应的信号之后自动向自己发出一个 QStateMachine::SignalEvent ;类似 地,对于 QObject 事件迁移动作( QEventTransition 对象) , 会发出一个 QStateMachine::WrappedEvent 。
妳可使用 QStateMachine::postEvent ()来向状态机发出自 己 的事件。
在向状态机发送自定义事件时,妳通常也 会有一个或多个自定义的可由该类型的事件触发的迁移动作。 要创建这样的迁移动作的话, 则,继承 QAbstractTransition 并且覆盖 QAbstractTransition::eventTest ()方法 , 在该方法中检查事件 的类型, 是否匹配妳的事件类型 ( 以及,可能还需要检查其它条件,例如,事件对象的某些属性 ) 。
我们来定义一个自定义的事件类型, StringEvent ,用来向状态机发送字符串:
struct StringEvent : public QEvent
{
StringEvent(const QString &val)
: QEvent ( QEvent ::Type( QEvent ::User+1)),
value(val) {}
QString value;
};
然后,定义一个迁移动作,只有当事件的字符串内容与某个特定字符串相匹配时才被触发(即为一个 带保护条件的 迁移动作):
class StringTransition : public QAbstractTransition
{
Q_OBJECT
public:
StringTransition(const QString &value)
: m_value(value) {}
protected:
virtual bool eventTest( QEvent *e)
{
if (e->type() != QEvent ::Type( QEvent ::User+1)) // StringEvent
return false;
StringEvent *se = static_cast<StringEvent*>(e);
return (m_value == se->value);
}
virtual void onTransition( QEvent *) {}
private:
QString m_value;
};
在eventTest()实现代码中,首先检查事件类型是否是我们所关心的那个类型;如果是的,则,将该事件转换为StringEvent,然后进行字符串比较。
下图是一个使用自定义事件和迁移动作的状态机:
以下是这个状态图的实现代码:
QStateMachine machine;
QFinalState *done = new QFinalState ();
StringTransition *t1 = new StringTransition("Hello");
t1->setTargetState(s2);
s1->addTransition(t1);
StringTransition *t2 = new StringTransition("world");
t2->setTargetState(done);
s2->addTransition(t2);
machine.addState(s1);
machine.addState(s2);
machine.addState(done);
machine.setInitialState(s1);
启动状态机之后,我们就可以向它发送事件。
machine.postEvent(new StringEvent("Hello"));
machine.postEvent(new StringEvent("world"));
一个未由任何迁移动作处理的事件,会被状态机安静地消化掉。可以将状态分组,并且提供一个默认手段来处理这种事件,这狠有用;例如,看下图所示的状态图:
对于深层嵌套的状态图,妳可以在最适当的层级上加入这种“备用”迁移动作。
在某 些 状态机中,可能需要特别关注 在某些状态下对于属性的赋值动作, 而不需要太关心在该状态不再活跃时对于属性值的恢复。如果 妳知道, 当状态机进入了某种 不会显式 地给某个属性设置一个值的状态时, 该属性应当被恢复 为初始值的话, 则, 妳可以将全局的恢复策略设置为 QStateMachine::RestoreProperties 。
QStateMachine machine;
machine.setGlobalRestorePolicy( QStateMachine ::RestoreProperties);
在设置了这个恢复策略的情况下,状态机会自动地恢复所有的属性。如果它进入了某个状态,而该状态下某个特定的属性未被指定,则,它会首先沿着层级关系向所有的祖先状态进行搜索,检查该属性是否在某个祖先中被设置了。如果有的话,则,该属性会被恢复为离该状态最近的祖先状态所定义的值。如果没有的话,则,它会被恢复为初始值(也就是说, 在 于状态中 执行任何属性赋值动作之前 ,该属性的值。)
研究以下代码:
QStateMachine machine;
machine.setGlobalRestorePolicy( QStateMachine ::RestoreProperties);
s1->assignProperty(object, "fooBar", 1.0);
machine.addState(s1);
machine.setInitialState(s1);
machine.addState(s2);
假设 , 当状态机启动时,属性 fooBar 的值是 0.0 。 当状态机处于状态 s1 时 ,该属性会被设置为 1.0 ,因为 该状态显式地设置了它的值。 当状态机处于状态 s2 时,没有 为该属性显示地定义值,因此它会被隐式地恢复为0.0。
如果我们使用嵌套状态的话,则,亲代状态为某个属性定义的值,会被所有未显式地给该属性赋值的子代状态继承。
QStateMachine machine;
machine.setGlobalRestorePolicy( QStateMachine ::RestoreProperties);
s1->assignProperty(object, "fooBar", 1.0);
machine.addState(s1);
machine.setInitialState(s1);
s2->assignProperty(object, "fooBar", 2.0);
s1->setInitialState(s2);
这个示例中, s1 拥有两个子代状态: s2 和 s3 。 当 s2 被进入时,属性 fooBar 的值会是 2.0 ,因为 , 在该状态中会显式地设置它。 当 状态 机进入状态 s3 的时候,由于 该状态中未定义该属性的值, 而 s1 将该属性的值定义为 1.0 了,所以 ,fooBar 会具有这个值。
状态机应用编程接口可与Qt 中的动画应用编程接口配套使用,以实现,在状态中对属性进行赋值时,以动画来展示。
比如以下代码:
s1->assignProperty(button, "geometry", QRectF (0, 0, 50, 50));
s2->assignProperty(button, "geometry", QRectF (0, 0, 100, 100));
s1->addTransition(button, SIGNAL(clicked()), s2);
我们为用户界面定义了两个状态。 在 s1 状态下,按钮( button )的尺寸较小, 在 s2 状态下,按钮的尺寸较大。 当我们点击按钮以从 s1 切换到 s2 时,按钮 的尺寸会随着状态 的进入而 立即改变。然而 ,如果我们想让这个迁移过程 更平滑的话,则, 只需要创建一个 QPropertyAnimation ,并且将它添加到迁移动作对象即可。
s1->assignProperty(button, "geometry", QRectF (0, 0, 50, 50));
s2->assignProperty(button, "geometry", QRectF (0, 0, 100, 100));
QSignalTransition *transition = s1->addTransition(button, SIGNAL(clicked()), s2);
transition->addAnimation(new QPropertyAnimation (button, "geometry"));
向相关的属性添加一个动画,意味着,当进入对应的状态时,该属性的赋值不是立即生效的。而是,当进入该状态时,动画会开始播放,并且平滑地对该属性进行动画式赋值。由于我们并未为该动画设置起始值和终止值,因此,它们会被隐式地设置。动画的开始值就是动画开始时该属性的当前值,而结束值就是在状态中定义的属性赋值动作中设置的值。
如果状态 机的全局恢复策略被设置为 QStateMachine::RestoreProperties ,那么,也可以 为属性的恢复动作添加动画。
在使用动画进行属性赋值的情况下,状态就不再能够在状态机进入该状态时为某个属性定义一个精确的值了。当动画运行的过程中,该属性可能具有潜在的任意值,具体的值取决于动画。
在某些情况下,如果能够检测到该属性的值在什么时候被实际设置成该状态所定义的值的话,会是狠有用的。
研究以下代码:
QMessageBox *messageBox = new QMessageBox (mainWindow);
messageBox->addButton( QMessageBox ::Ok);
messageBox->setText("Button geometry has been set!");
messageBox->setIcon( QMessageBox ::Information);
s2->assignProperty(button, "geometry", QRectF (0, 0, 50, 50));
connect(s2, SIGNAL(entered()), messageBox, SLOT(exec()));
s1->addTransition(button, SIGNAL(clicked()), s2);
当按钮 ( button ) 被点击时,状态 机会迁移到状态 s2 , 它会设置该按钮的尺寸,然后弹出消息 框,告知用户,按钮的尺寸已经改变。
在普通的情况下,没有使用动画, 这砣代码能够正常工作。但是,如果 在 s1 到 s2 之间的迁移动作上为按钮( button )的 geometry 属性设置了动画,那么, 在进入状态 s2 时,动画会启动,但是 ,在动画结束之前, geometry 属性 不会真正达到预 先 定义 的值。 在这种情况下,消息 框会在按钮的尺寸真正被设置为目标值之前就弹出。
为了确保消息框不要在按钮尺寸实际达到最终值之前弹出,我们可以使用该状态对象的propertiesAssigned()信号。当属性被设置为最终值的时候,就会发射propertiesAssigned()信号,至于它是立即被设置的还是在动画终止之后被设置的,都没有关系。
QMessageBox *messageBox = new QMessageBox (mainWindow);
messageBox->addButton( QMessageBox ::Ok);
messageBox->setText("Button geometry has been set!");
messageBox->setIcon( QMessageBox ::Information);
s2->assignProperty(button, "geometry", QRectF (0, 0, 50, 50));
connect(s3, SIGNAL(entered()), messageBox, SLOT(exec()));
s1->addTransition(button, SIGNAL(clicked()), s2);
s2->addTransition(s2, SIGNAL(propertiesAssigned()), s3);
在这个示例中,当按钮( button )被点击时,状态 机会进入 s2 状态。 它会持续处于 s2 状态,直到 geometry 属性 被设置为 QRect(0, 0, 50, 50) 。 然后,它会迁移到状态 s3 。进入 s3 之后 ,消息框就会弹出。如果指向 s2 的那个迁移动作中包含了一个针对 geometry 属性的动画,则,状态 机会保持 在 s2 状态,直到动画完毕 。如果 没有这么一个动画,那么 , 它就会简单地设置 该属性,并且立即进入状态 s3 。
无论采用哪种方式 , 妳都可以确信, 当状态机进入状态 s3 的时候, geometry 属性 会被设置为默认 值 。
如果全局 的恢复策略被设置为 QStateMachine::RestoreProperties ,则,只有 当上面所说的动作被执行之后,该状态对象才会发射propertiesAssigned()信号。
如果某个状态对象带有属性赋值动作,并且,指向该状态的迁移动作中带有针对该属性的动画,则,可能会在该属性的值被动画式赋值为该状态定义的值之前,该状态就已经退出了。尤其是,存在一些从该状态对象发出的不依赖于propertiesAssigned 信号的迁移动作时,就会如此,前一小节就说明过这种情况。
状态机应用编程接口会确保,由状态机所赋值的属性,会处于以下两种情况中的一种:
•. 会具有一个显式赋值给它的值。
•.当前正处于动画式变更成一个被显式赋值给它的值的过程中。
当一个状态在动画结束之前就退出时,状态机的行为取决于该迁移动作的目标状态。如果目标状态显式地给该属性赋予了一个值,则,不会进行额外的动作。该属性会被赋予由目标状态对象定义的值。
如果目标状态对象未对该属性进行赋值,则,有两种可能性:默认情况下,该属性会被赋予由刚刚退出的状态所定义的值(在该动画照常结束之后被设置的值)。如果设置了全局的恢复策略,则,这个策略会优先生效,于是该属性会被恢复。
之前已经说过,妳可以向迁移动作中加入动画,以确保,在目标状态中的属性赋值是以动画形式展现的。如果妳希望,针对特定的属性,会使用某个特定的动画,而不管它是位于哪个迁移动作中,那么,妳可以将它添加为状态机中的默认动画。对于这种情况,狠有用:在状态机被构建时,特定状态对象所赋值(或恢复)的属性还是未知的。
s2->assignProperty(object, "fooBar", 2.0);
s1->addTransition(s2);
QStateMachine machine;
machine.setInitialState(s1);
machine.addDefaultAnimation(new QPropertyAnimation (object, "fooBar"));
当状态机进入状态 s2 的时候, 会播放针对属性 fooBar 的默认动画,因为 s2 对该动画进行了赋值。
注意,针对迁移动作显式设置的动画,会比针对该属性设置的默认动画的优先级高。
QStateMachine 是 QState 的一个子类。 这样,状态机本身就可以成为另一个状态机的子状态。 QStateMachine 覆盖 了 QState::onEntry ()方法 ,它会调用 QStateMachine::start () ,因此, 当子状态机被进入时, 会自动开始运行。
亲代状态 机会 在状态机算法中将子状态机作为一个 原子 状态进行处理。 子状态机是自包含的; 它维护着自己的事件队列和配置信息。尤其 是要注意, 子状态机的 配置 并不是亲代状态机的配置的一部分(仅仅 子状态机本身是亲代状态机的一部分 )。
子状态机中的状态不可以被指定为亲代状态 机中迁移动作的目标;只有 子状态机本身可以这样做。 同样地,亲代状态机中的状态也不可以被指定为子状态机中的迁移动作的目标。 子状态机的 finished ()信号可用来触发亲代状态机中的迁移动作。
闺蜜
女人旺夫面相解析
Your opinionsHxLauncher: Launch Android applications by voice commands